Refactor(Network):用 HttpClient 代替已经过时的 WebRequest、WebClient、ServicePointManager#6407
Conversation
pref(ModNet):使用 HttpClient 替代过时的 WebRequest 和 WebClient
|
先上了一点测试,发现了比较有意思的现象,使用 HttpClient 的版本下载速度要快于使用 WebRequest 的版本,包括我还拿了自己写的核心库和官版比较,结果还是核心库更快一不止点(而且核心库还是一个文件一个线程,一分钟就装完了,PCL 卡半天还没好)
经过验证,似乎是 WebRequest 性能太差了.... |
CO-authored-by: qiuyue712 <qiuyue712@outlook.com>
|
是否可以同时 Resolve 掉 #6467? @shimoranla |
尝试在 WebRequest 下设置 CachePolicy 为 Revalidate,完全没有 304 响应,看起来 WebRequest 不会加 If-None-Match 或 If-Modified-Since 标头 如果需要自己处理的话那这玩意折腾起来比较坐牢,需要在加载器的缓存保留 ETag 标头,这样整个地方都要改成使用 HttpRequestMessage 才能正确处理 304 响应 看龙猫短期内有没有意向做掉,如果没有的话我再考虑折腾 cc @LTCatt |
|
为啥 Build Failed 了?
算了 Rebase 得了 |
|
在能稳定复现的计算机上使用构建产物进行全量下载,速度恢复正常(基本吃满带宽) |
这个和 #6467 都是优先的( |
baiyuexiao496
left a comment
There was a problem hiding this comment.
这下面一堆 Return 改 Exit Sub 真的对吗.....
CO-Authored-By: baiyuexiao496 <mibaiyuexiao@outlook.com>
Co-authored-by: tangge233 <50769997+tangge233@users.noreply.github.com>
|
因为没注释,问点问题……
多谢! |
(寄)
因为微软文档上写的 HttpClient 实际上会一直存储域名解析信息,如果用户开长一点就会出现缓存解析导致连接出问题,所以当时写的时候做了轮换 不过合并前一段时间看了 ce 日志,这东西底层还是 HttpWebRequest 貌似不会这个有问题,所以理论上也可以删掉换成单例 (.net fw 只是加了异步,网络栈是在 .NET Core 重写的,这些我后来才了解到) btw: HttpClientFactory 没有被更换,只是更换了内部的 Client
不清楚能不能在 UseProxy=True 的情况下不传 Proxy,所以保险起见直接打了个来确保代理正常工作 做这个类目的之一是适配 #5770 可以手动更换代理地址,正如我在 https://github.com/Meloong-Git/PCL/issues/6418#issuecomment-2916523442提到的那样,因为没办法在实例化 HttpClient 后动态修改实例内部的 Handler,所以代理地址会被硬编码在里面直到 6 小时后被销毁然后重新创建
这个只是觉得每个 Client 都得来个 .GetAwaiter.GetResult() 重复太多了所以糊了个类弄这些东西
|
| Catch ex As ThreadInterruptedException | ||
| Throw | ||
| Catch ex As TaskCanceledException | ||
| Return New WebException($"连接服务器超时,请检查你的网络环境是否良好({ex.Message},{Url})", ex) |
There was a problem hiding this comment.
Return New WebException???????
WTF?????????
There was a problem hiding this comment.
Return New WebException??????? WTF?????????
当时应该是忘记关掉 Copilot 的补全了 Orz
| ''' <param name="ContentType">请求的套接字类型。</param> | ||
| ''' <param name="DontRetryOnRefused">当返回 40x 时不重试。</param> | ||
| Public Function NetRequestRetry(Url As String, Method As String, Data As Object, ContentType As String, Optional DontRetryOnRefused As Boolean = True, Optional Headers As Dictionary(Of String, String) = Nothing) As String | ||
| Public Function NetRequestRetry(Url As String, Method As String, Data As Object, ContentType As String, Optional DontRetryOnRefused As Boolean = True, Optional Headers As Dictionary(Of String, String) = Nothing, Optional RequireReturnRespObj As Boolean = False) As String |
There was a problem hiding this comment.
RequireReturnRespObj 永远是 false,why?
There was a problem hiding this comment.
看了一圈只有多线程引擎那里用了 RequireReturnRespObj 参数,这应该把多线程引擎那里单独写一份,搞不懂为啥要在 NetRequestRetry 加这个参数
| ' 获取HttpMethod | ||
| Private Function GetHttpMethod(Method As String) As HttpMethod | ||
| Select Case Method?.Trim().ToUpperInvariant() | ||
| Case "HEAD" | ||
| Return HttpMethod.Head | ||
| Case "POST" | ||
| Return HttpMethod.Post | ||
| Case "PUT" | ||
| Return HttpMethod.Put | ||
| Case "DELETE" | ||
| Return HttpMethod.Delete | ||
| Case Else | ||
| Return HttpMethod.Get | ||
| End Select | ||
| End Function |
There was a problem hiding this comment.
它有自带方法……
New HttpMethod(Method.Trim.ToUpper)| Dim Client = ClientFactory.GetHttpClient() | ||
| Dim Resp As HttpResponseMessage = Client.SendAsync(Req, HttpCompletionOption.ResponseHeadersRead, CTS.Token).GetAwaiter().GetResult() | ||
|
|
||
| If RequireReturnRespObj Then Return Resp |
There was a problem hiding this comment.
我没有找到任何一个地方需要用到 RequireReturnRespObj 的,这一堆处理是什么原因?
| Private Request As New HttpRequest() | ||
| Public Class HttpRequest | ||
| Public Sub New() | ||
| End Sub |
There was a problem hiding this comment.
它只有静态的 Function,为什么要做成单例?
CSharp 写习惯了所以干啥都 Public Class Orz
| SecretHeadersSign(Url, Request, UseBrowserUserAgent) | ||
| Return Request.DownloadString(Url) | ||
| Using Req As HttpRequestMessage = Request.GetRequestMessage(Url, Headers:=Headers, RequireHeaderSign:=True, RequireCdnSign:=True, UseBrowserUA:=UseBrowserUserAgent) | ||
| Req.Headers.AcceptLanguage.Add(New StringWithQualityHeaderValue("en-US,en;q=0.5")) |
There was a problem hiding this comment.
完全运行不了,在跑 OptiFine 版本列表获取时报错。
| Req.Headers.AcceptLanguage.Add(New StringWithQualityHeaderValue("en-US,en;q=0.5")) | |
| Req.Headers.AcceptLanguage.Add(New StringWithQualityHeaderValue("en-US", 0.5)) |
There was a problem hiding this comment.
完全运行不了,在跑 OptiFine 版本列表获取时报错。
草为什么我本地测没问题啊(
There was a problem hiding this comment.
加载线程 DlOptiFineList Official (36) 出错,已完成 2%:获取结果失败,值“en-US,en”的格式无效。(https://optifine.net/downloads)
在 PCL.ModNet.NetGetCodeByClient(String Url, Encoding Encoding, String Accept, Boolean UseBrowserUserAgent) 位置 F:\Projects\Plain Craft Launcher 2\本体代码\Plain Craft Launcher 2\Modules\Base\ModNet.vb:行号 131
在 PCL.ModDownload.DlOptiFineListOfficialMain(LoaderTask`2 Loader) 位置 F:\Projects\Plain Craft Launcher 2\本体代码\Plain Craft Launcher 2\Modules\Minecraft\ModDownload.vb:行号 380
在 PCL.ModLoader.LoaderTask`2._Closure$__13-0._Lambda$__0() 位置 F:\Projects\Plain Craft Launcher 2\本体代码\Plain Craft Launcher 2\Modules\Base\ModLoader.vb:行号 319
错误类型:System.Net.WebException
→ 值“en-US,en”的格式无效。
在 System.Net.Http.Headers.HeaderUtilities.CheckValidToken(String value, String parameterName)
在 System.Net.Http.Headers.StringWithQualityHeaderValue..ctor(String value, Double quality)
在 PCL.ModNet.NetGetCodeByClient(String Url, Encoding Encoding, Int32 Timeout, String Accept, Boolean UseBrowserUserAgent) 位置 F:\Projects\Plain Craft Launcher 2\本体代码\Plain Craft Launcher 2\Modules\Base\ModNet.vb:行号 140
错误类型:System.FormatException
| Using Client As New WebClient | ||
| Try | ||
| SecretHeadersSign(Url, Client, UseBrowserUserAgent) | ||
| 'SecretHeadersSign(Url, Client, UseBrowserUserAgent) |
There was a problem hiding this comment.
Why?
GetRequestMessage 会自己做这个任务(
There was a problem hiding this comment.
但是这里根本没有调
GetRequestMessage啊?
不建议代码挤一起的原因 +1:容易漏掉某些东西 Orz
| Throw New WebException($"获取到的分段大小不一致:Range 起始于 {Info.DownloadStart},预期 ContentLength 为 {FileSize - Info.DownloadStart},返回 ContentLength 为 {ContentLength},总文件大小 {FileSize}") | ||
| End If | ||
| 'Log($"[Download] {LocalName} {Info.Uuid}#:通过大小检查,文件大小 {FileSize},起始点 {Info.DownloadStart},ContentLength {ContentLength}") | ||
| Log($"[Download] {LocalName} {Info.Uuid}#:通过大小检查,文件大小 {FileSize},起始点 {Info.DownloadStart},ContentLength {ContentLength}") |
| State = NetState.Merge | ||
| Else | ||
| Return | ||
| Exit Sub |
There was a problem hiding this comment.
???
这后面一堆应该是当时让 Copilot 重新整理代码代码的时候 Copilot 批量替换了,没仔细审代码 Orz
| Dim Info As New FileInfo(LocalPath) | ||
| Info.Directory.Create() |
|
好像写混了,当时没认真看 Orz |
| If RequestMessage.Content.Headers.Contains(Header.Key) Then Continue For | ||
| If Not RequestMessage.Content.Headers.Contains(Header.Key) Then |
| If Data Is Nothing OrElse RequestMessage.Content Is Nothing Then Continue For | ||
| If RequestMessage.Content.Headers.Contains(Header.Key) Then Continue For | ||
| If Not RequestMessage.Content.Headers.Contains(Header.Key) Then | ||
| If Header.Key.ToLower() = "Content-Type" Then RequestMessage.Content.Headers.ContentType = New MediaTypeHeaderValue(Header.Value) |
| Else | ||
| ex = New ResponsedWebException($"服务器返回错误({ex.Status},{ex.Message},{Url}){vbCrLf}{Res}", Res, ex) | ||
| If Not ReqHeaders.ContainsKey("Content-Type") Then ReqHeaders.Add("Content-Type", ContentType) | ||
| End If |
There was a problem hiding this comment.
这一堆全没啦?
ResponsedWebException 在判密码错误的时候还要用的……


PR 改动
技术性更改
将 WebRequest 和 WebClient 替换为 HttpClient 以实现更高的网络性能
支持在发送网络请求时读取 GZip 等压缩格式的响应以节约双方的网络传输成本
可能修复了以下 Bug
Resolve #6418
特别感谢(重构期间帮了大忙)
@wuliaodexiaoluo (帮助解决了标头误用的问题)
@sulingjiang(提出了可以实现继承了 IWebProxy 类来动态修改代理的想法)
@qiuyue712(协助测试)
@jiangyanyue(协助测试期间偶然发现了 CTS 超时会作用于整个请求流程,以及一个下载的 Bug)